package com.qlogic.commons.platform.impls.scheduler;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * This class implements an jobs manager similar to Unix <code>cron</code>
 * and <code>at</code> daemons. It is intended to fire events when jobs'
 * date and time match the current ones. Jobs are added dynamically and can be
 * one-shot or repetitive (i.e. rescheduled when matched). Time unit is seconds.
 * Jobs scheduled less than one second to the current time are rejected (a
 * <code>PastDateException</code> is thrown).
 * <p>
 * 
 * The job scheduler has been designed to manage a large quantity of jobs
 * (it uses a priority queue to optimize job dates selection) and to reduce
 * the use of the CPU time (the JobsManager's thread is started only when there
 * are jobs to be managed and it sleeps until the next job date).
 * <p>
 * 
 * Note : because of clocks' skews some job dates may be erroneous,
 * particularly if the next job date is scheduled for a remote time (e.g. more
 * than a few days). In order to avoid that problem, well-connected machines can
 * use the <a href="ftp://ftp.inria.fr/rfc/rfc13xx/rfc1305.Z">Network Time
 * Protocol</a> (NTP) to synchronize their clock.
 * <p>
 * 
 * Example of use:
 * 
 * <pre>
 * // Creates a new JobsManager
 * JobsManager mgr = new JobsManager();
 * 
 * // Date job (non repetitive)
 * mgr.addJob(&quot;fixed5min&quot;, new Date(System.currentTimeMillis() + 300000),
 * 		new JobListener() {
 * 			public void handleJob(Job entry) {
 * 				System.out.println(&quot;5 minutes later&quot;);
 * 			}
 * 		});
 * 
 * Calendar cal = Calendar.getInstance();
 * cal.add(Calendar.WEEK_OF_YEAR, 1);
 * mgr.addJob(&quot;week_one&quot;, cal.getTime(), new JobListener() {
 * 	public void handleJob(Job entry) {
 * 		System.out.println(&quot;One week later&quot;);
 * 	}
 * });
 * 
 * // Job with a delay (in minute) relative to the current time.
 * mgr.addJob(1, true, new JobListener() {
 * 	public void handleJob(Job entry) {
 * 		System.out.println(&quot;1 more minute ! (&quot; + new Date() + &quot;)&quot;);
 * 	}
 * });
 * 
 * // Cron-like job (minute, hour, day of month, month, day of week, year)
 * // Repetitive when the year is not specified.
 * 
 * mgr.addJob(-1, -1, -1, -1, -1, -1, new JobListener() {
 * 	public void handleJob(Job entry) {
 * 		System.out.println(&quot;Every minute (&quot; + new Date() + &quot;)&quot;);
 * 	}
 * });
 * 
 * mgr.addJob(5, -1, -1, -1, -1, -1, new JobListener() {
 * 	public void handleJob(Job entry) {
 * 		System.out.println(&quot;Every hour at 5' (&quot; + new Date() + &quot;)&quot;);
 * 	}
 * });
 * 
 * mgr.addJob(00, 12, -1, -1, -1, -1, new JobListener() {
 * 	public void handleJob(Job entry) {
 * 		System.out.println(&quot;Lunch time (&quot; + new Date() + &quot;)&quot;);
 * 	}
 * });
 * 
 * mgr.addJob(07, 14, 1, Calendar.JANUARY, -1, -1, new JobListener() {
 * 	public void handleJob(Job entry) {
 * 		System.out.println(&quot;Happy birthday Lucas !&quot;);
 * 	}
 * });
 * 
 * mgr.addJob(30, 9, 1, -1, -1, -1, new JobListener() {
 * 	public void handleJob(Job entry) {
 * 		System.out.println(&quot;On the first of every month at 9:30&quot;);
 * 	}
 * });
 * 
 * mgr.addJob(00, 18, -1, -1, Calendar.FRIDAY, -1, new JobListener() {
 * 	public void handleJob(Job entry) {
 * 		System.out.println(&quot;On every Friday at 18:00&quot;);
 * 	}
 * });
 * 
 * mgr.addJob(00, 13, 1, Calendar.AUGUST, -1, 2001, new JobListener() {
 * 	public void handleJob(Job entry) {
 * 		System.out.println(&quot;2 years that this class was programmed !&quot;);
 * 	}
 * });
 * </pre>
 * 
 */

public class JobsManager {

	protected JobWaiter waiter;

	protected SortedSet queue; ///set of JobEntries 

	private boolean debug = false;
	
	protected String name = "JobsManager";

	private void debug(String s) {
		if (debug) {
			System.out.println("[" + Thread.currentThread().getName()
					+ "] " + name + " : " + s);
		}
	}

	/**
	 * Creates a new JobsManager. The waiter thread will be started only when
	 * the first job listener will be added.
	 * 
	 * @param isDaemon
	 *            true if the waiter thread should run as a daemon.
	 * @param threadName
	 *            the name of the waiter thread
	 */
	public JobsManager(boolean isDaemon, String threadName) {
		queue = new TreeSet();
		waiter = new JobWaiter(this, isDaemon, threadName);
	}

	/**
	 * Creates a new JobsManager. The waiter thread will be started only when
	 * the first job listener will be added. The waiter thread will <i>not</i>
	 * run as a daemon.
	 */
	public JobsManager(boolean isDaemon) {
		this (isDaemon, "JobsManager");
	}

	/**
	 * Creates a new JobsManager. The waiter thread will be started only when
	 * the first job listener will be added. The waiter thread will <i>not</i>
	 * run as a daemon.
	 */
	public JobsManager() {
		this (false);
	}

	/**
	 * Adds an job for a specified Job
	 * 
	 * @param entry
	 *            the Job.
	 * @exception PastDateException
	 *                if the job date is in the past (or less than one second
	 *                away from the current date).
	 */
	public synchronized void addJob (Job entry)
			throws PastDateException {
		debug("Add a new job entry : " + entry);
		queue.add(entry);
		if (queue.first().equals (entry)) {
			debug("This new job is the top one, update the waiter thread");
			waiter.update(entry.getRunningTime ());
		}
	}
	
	public synchronized void addJob (String name, JobScheduler scheduler, JobListener listener) 
			throws PastDateException {
		addJob (new Job (name, scheduler, listener));
	}

	/**
	 * Removes the specified Job.
	 * 
	 * @param entry
	 *            the Job that needs to be removed.
	 * @return <code>true</code> if there was an job for this date,
	 *         <code>false</code> otherwise.
	 */
	public synchronized boolean removeJob(Job entry) {
		boolean found = false;
		if (!queue.isEmpty()) {
			Job was_first = (Job) queue.first();
			found = queue.remove(entry);

			// update the queue if it's not now empty, and the first job has
			// changed
			if (!queue.isEmpty() && entry.equals(was_first)) {
				waiter.update(((Job) queue.first()).getRunningTime ());
			}
		}
		return found;
	} 

	/**
	 * Removes all the jobs. No more jobs, even newly added ones, will be
	 * fired.
	 */
	public synchronized void removeAllJobs() {
		queue.clear();
	}

	/**
	 * Removes all the jobs. No more jobs, even newly added ones, will be
	 * fired.
	 */
	public synchronized void removeAllJobsAndStop() {
		waiter.stop();
		waiter = null;
		queue.clear();
	}

	public boolean isStopped() {
		return (waiter == null);
	}

	/**
	 * Tests whether the supplied Job is in the manager.
	 * 
	 * @param Job
	 * @return boolean whether Job is contained within the manager
	 */
	public synchronized boolean containsJob(Job jobEntry) {
		return queue.contains (jobEntry);
	}
	
	public void rebuidJob (Job jobEntry) throws PastDateException {
		jobEntry.setEnabled (false);
		removeJob (jobEntry);
		jobEntry.setScheduler (jobEntry.getScheduler ());
		jobEntry.setEnabled (true);
		addJob (jobEntry);
	}

	/**
	 * Returns a copy of all jobs in the manager.
	 */
	public synchronized List getAllJobs() {
		List result = new ArrayList();
		Iterator iterator = queue.iterator();
		while (iterator.hasNext()) {
			result.add(iterator.next());
		}

		return result;
	}

	/**
	 * This is method is called when an job date is reached. It is only be
	 * called by the the JobWaiter or by itself (if the next job is less
	 * than 1 second away).
	 */
	protected synchronized void ringNextJob() {
		debug("ringing next job");

		// if the queue is empty, there's nothing to do
		if (queue.isEmpty()) {
			return;
		}
		
		debug("queue is not empty");

		// Removes this job and notifies the listener
		Job entry = (Job) queue.first();
		debug("removing first job " + entry.getName ());
		queue.remove(entry);
		
		// NOTE: if the entry is still running when its next job time comes
		// up,
		// that execution of the entry will be skipped.
		if (entry.isRingInNewThread()) {
			new Thread(new RunnableRinger(entry)).start();
		} else {
			// ring in same thread, sequentially.. can delay other jobs
			debug("ring in same thread");
			try {
				entry.process ();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		// Reactivates the job if it is repetitive
		if (entry.isRepeating ()) {
			entry.updateRunningTime();
			queue.add(entry);
		}

		// Notifies the JobWaiter thread for the next job
		if (queue.isEmpty()) {
			debug("no more jobs to handle; queue is empty");
		} else {
			long jobTime = ((Job) queue.first()).getRunningTime ();
			if (jobTime - System.currentTimeMillis() < 1000) {
				debug("next job is within 1 sec or already past - ring it without waiting");
				ringNextJob();
			} else {
				debug("updating the waiter for next job: " + queue.first());
				waiter.restart(jobTime);
			}
		}
	} // notifyListeners()

	/**
	 * Stops the waiter thread before ending.
	 */
	public void finalize() {
		if (waiter != null)
			waiter.stop();
	}

	/**
	 * Used to ring an Job in a new Thread.
	 * 
	 * @see com.jtheory.jdring.Job#setRingInNewThread()
	 */
	private class RunnableRinger implements Runnable {
		Job entry = null;

		RunnableRinger(Job entry) {
			this.entry = entry;
		}

		public void run() {
			try {
				entry.process ();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	public boolean isDebug() {
		return debug;
	}

	public void setDebug(boolean debug) {
		this.debug = debug;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}
